jactionlint 2.2.0 adds about thirty new rules and extends many existing ones, mostly for script injection, token permissions, pinning and runner limits. It also adds CLI modes for pull request checks (--changed-since), rule discovery (--list-rules, --explain, shell completions), fact export and timeout suggestions, and publishes wheels to PyPI. Several new rules are in the default or correctness profile, so a run that was clean on 2.1.0 can show new findings. See Changed before you upgrade.
Highlights
- More checks before a run fails: shell syntax errors without shellcheck, JavaScript syntax errors in
actions/github-script, job permissions too narrow for the actions in the job,ghwithout a token,ubuntu-slimlimits,github.eventproperty typos, and validation of GitHub's Actions lock file. - Deeper security analysis:
untrusted-checkout,cache-poisoning,template-injectionand the secret rules follow more code paths. New pedantic and online rules cover command and argument injection, repojacking, release age, unpinned action dependencies and environments without required reviewers. - Better for CI and tooling: report only findings on changed lines, select single rules, export the repository model as JSON, get JSON Schemas for config and output, and install with
pip/uvx.
Added
New rules in the default and correctness profiles
shell-syntax(default, error): reports syntax errors inbash/shrun:scripts, such as an unclosed quote or a missingfi/done, using a built-in parser. It runs only when shellcheck does not run, so the two never report the same error. (#273, @jdx)insufficient-permissions(default, error): reports a job whose explicitpermissions:miss a scope that one of its actions needs (upload-sarif,deploy-pages, OIDC logins,gh release create,git pushand others, about 26 entries).missing-permissions --fixnow uses the same table to write job-level permissions when the needs are exact. (#192, @jdx)ubuntu-slim-limits(default, error): reportscontainer:,services:and Docker container actions in jobs that run only onubuntu-slim. Withpedantic, it also reportstimeout-minutesabove 15.missing-timeout --fixlimits its value to 15 for these jobs. (#190, @jdx)actions-lock(default, error) andactions-lock-drift(online): check.github/workflows/actions.lockagainst the workflows. They report missing, stale and malformed entries. With--online, they also report replaced repositories, moved tags and commits that are on no branch or tag. Repositories without a lock file are not affected.unpinned-usesaccepts a ref that the lock file pins to a commit. (#237, #228, @jdx)hardcoded-registry-credentials(default, error): reports plain-text passwords, tokens and keys inregistriesofdependabot.yml, including passwords embedded in a registry URL. (#265, @jdx)gh-without-token(correctness): reports aghcommand that needs a token in a step where noGH_TOKEN/GITHUB_TOKENis set. (#177, @jdx)environment-file-redirect(correctness): reports writes toGITHUB_OUTPUT(and similar files) without the$, and a>that overwrites data the same script already wrote. It has a fix: safe for the missing$, unsafe for>. (#184, @jdx)github-script-syntax(correctness): checks thescriptinput ofactions/github-scriptwithnode --check. It knows the argument names of each action version, so it catches errors likeconst glob = require(...). It needs Node.js 20 or later onPATH; without it, the rule is skipped and a note says so. (#242, @jdx)malformed-expression-delimiters(correctness): reports${ github.ref },$ {{ x }}and{{ github.sha }}. (#180, @jdx)multiline-plain-run(correctness): reports a multi-linerun:that YAML folds into a single line. (#205, #256, @jdx)concurrency-deadlock(correctness): reports a concurrency group that a workflow shares with a local reusable workflow it calls. GitHub cancels such runs as a deadlock. (#210, @jdx)conflicting-action-inputs(correctness), plus newmissing-action-inputcases: report input combinations of well-known actions that cannot work, for exampledownload-artifactnamewithartifact-ids, ornick-fields/retrywithout exactly one timeout input. (#207, @jdx)undefined-propertynow checksgithub.event.*property names against the webhook payloads, down to three levels. By default, it reports only names that no event has. Thestrict-event-payloadsoption, on in the pedantic profile, also reports properties that the workflow's own triggers don't have. (#249, @jdx)
New pedantic rules
deprecated-runner-label: warns about runner labels with an announced retirement date (macos-14,ubuntu-22.04), with replacement labels. (#182)deprecated-node-runtime: reportsruns.using: node20/16/12in action metadata, popular actions at majors that declare node20, and environment variables that select the Node.js runtime. (#225, #230, #234)command-injection: reports untrusted input that was moved toenv:but then runs as code (eval,bash -c,python -c,execSyncin github-script, and similar). (#202)argument-injection: reports free text in the program ofsed/awk, infind -exec, or as a leading word oftar,curl,git cloneand others. (#221)output-clobbering: reports input that can contain line breaks written toGITHUB_OUTPUTas a single record. (#212)comment-trigger-authorization: reports comment-triggered jobs that don't check who commented. (#217)event-name-never-received: reportsgithub.event_namecompared with an event the workflow never receives. (#214)fail-fast-action-inputs: reports actions configured to pass when they produced nothing. (#196)macos-gnu-flags: reports GNU-only options in run scripts of macOS jobs. (#241)unmatched-paths-filter: reportspathsfilters that match no file in the repository. (#238)missing-version-comment: reports a commit-SHA pin without a version comment.--online --fixadds the comment. (#232)unused-workflow-secret: reportson.workflow_call.secretsthat nothing reads. (#223)
All by @jdx.
New online rules
repojacking: reports an action or reusable workflow whose owner name anyone could register. It runs on github.com only. (#248, @jdx)unpinnable-action: reports an action pinned by hash whose own dependencies are not pinned. (#240, @jdx)action-release-cooldown(opt-in): reports an action at a version released less than N days ago. It is off until the config has an entry for it:(#235, @jdx)rules: action-release-cooldown: { level: warning, days: 7 }
Extensions to existing rules
-
untrusted-checkoutfollows the pull request revision through variables, script files,pull/N/headfetches, local reusable workflows and composite actions. It now coversissue_commentworkflows that build a pull request, and reports gates that pass once in front of code that can change. It honors the fork protection ofactions/checkout, and counts actions that run the workspace and the input that makes them do it. (#159, #175, #203, #206, #208, #213, #220, @jdx) -
With
--online, environments are checked:untrusted-checkout,untrusted-artifactandagentic-actionsno longer trust anenvironment:that has no required reviewers, or that doesn't exist in a public repository. (#247, @jdx) -
cache-poisoning:- A build job that uploads artifacts for a publishing job counts as a release job, even with a read-only token.
- More publishers are recognized, including
docker/build-push-actionwith an expression forpush, plus maturin and sccache caches,pnpm/setup, graalvm and composer. - Caches of credential files are reported.
- A new
pedanticoption adds zizmor's heuristics for tag-triggered and release-branch workflows.
-
template-injectionreports more outsider-controlled free text in the default profile: previous titles and bodies of edited issues, the fork homepage, wiki page titles, merge queue commit messages, andworkflow_runpaths. (#173, @jdx) -
dangerous-triggersmessages say which token scopes the workflow can write and which secrets it reads. (#189, @jdx) -
github-envfollows text that a script reads withghandgit. (#218, @jdx) -
unpinned-imageschecksdocker runimages in scripts and adds per-image policies and image allow/deny lists.--online --fix=unsafepins container images to a digest. (#211, #239, @jdx) -
unpinned-usesaccepts the tag of an immutable release under--online.--online --fixnames the most specific version tag in the comment it writes. (#251, #224, @jdx) -
ref-version-mismatchreads ratchet comments and can fix the version comment (unsafe). (#227, @jdx) -
workflow-secret-scopehas ajob-leveloption.secrets-outside-envnow reports secrets passed to reusable workflows. (#223, @jdx) -
More pedantic options:
unlocked-installreports npm/yarn/bun/aube installs that run lifecycle scripts or have no release-age cooldown.untrusted-artifactknows more download actions and reports downloads of every artifact.use-trusted-publishingreports jobs that build, have no environment, or drop attestations.unredacted-secretsreports values cut from a secret and printed by a script.concurrency-limitsreports schedule-only and manual-only workflows.
-
aube is recognized wherever npm, pnpm, yarn and bun are. (#254, @jdx)
-
Runner labels:
- Labels of Blacksmith, Depot, Namespace, RunsOn, Ubicloud and WarpBuild are accepted. Limit them with
self-hosted-runner.providers. - Labels built from a matrix value and text are checked.
self-hosted-runnercounts the configured labels and runner groups, and says when a pull request reaches the runner.
- Labels of Blacksmith, Depot, Namespace, RunsOn, Ubicloud and WarpBuild are accepted. Limit them with
-
github-appchecks two more token actions, anddependabot-missing-actions-updatecovers local composite actions. (#229, @jdx) -
superfluous-actionsresolvesruns-onexpressions that the repository defines. (#271, @jdx) -
missing-timeouthas anaccept-step-timeoutsoption for jobs whose steps all settimeout-minutes. (#252, @jdx)
Online checks
- With
--online, the inputs of any remote action outside the built-in data set are checked against itsaction.ymlat the pinned ref. Calls of reusable workflows in other repositories are checked the same way. (#231, #236, @jdx) - On GitHub Enterprise Server, actions from github.com can be looked up on github.com. (#250, @jdx)
--online-report=FILEwrites online coverage and cache freshness as JSON, for any output format. The SARIF output gets the same fields. (#166, @jdx)
CLI and configuration
--changed-since REVshows only findings on lines changed since the merge base withREV, for pull request checks.--changed-scope filesshows every finding in a changed file. The whole repository and call graph are still analyzed. A missing merge base, for example in a shallow clone, exits with status 3. (#274, #279, @jdx)jactionlint --changed-since origin/main
--select=RULESreports only the named rules.--no-ignoresdisables inline ignores, config ignores and the baseline.--list-ruleslists all rules, also as JSON. (#200, @jdx)--explain RULEprints a rule's documentation, options, fix prerequisites and examples, offline.--completion=bash|zsh|fish|powershellprints a shell completion script. (#204, #269, @jdx)--show-fix-statusshows under each finding whether a fix is available and how to apply it, and more fixable rules say why a finding has no fix. SARIF output explains a missing fix as well. (#269, #164, @jdx)--show-doc-urlsand--finding-context(events and token permissions on each JSON/SARIF finding) add details to the output.--ruff=CMDchecks Python scripts with ruff instead of pyflakes. The config keyplatform: forgejo|giteaaccepts full-URLuses:references to other hosts. Jujutsu (.jj) workspaces are detected as project roots. (#215, @jdx)--export-factsprints the events, permissions, concurrency, jobs, steps and calls of a repository as versioned JSON, forjqor policy tools.--print-facts-schemaprints its schema. (#272, @jdx)--suggest-timeoutsreports how long each job took in its latest successful runs and suggests atimeout-minutesvalue. It is a report only and changes no files. (#275, @jdx)- JSON Schemas for the config file and for
--format jsonoutput are available through--print-config-schemaand--print-output-schema.--init-configadds ayaml-language-serverschema comment. (#195, @jdx) - A new
shellcheck:config section setsrcfile,argsandenable. The--shellcheck-rcfile=FILEflag overridesrcfile. (#191, @jdx) - A note says when shellcheck, pyflakes or node did not run. (#188, @jdx)
Installation
- Wheels are published to PyPI, so jactionlint can be installed with
pip,pipx,uv tooloruvx. (#277, @jdx)uvx jactionlint
- The pre-commit hooks now also run when
action.ymlordependabot.ymlchanges. A newjactionlint-shellcheckhook builds shellcheck into the hook environment. (#181, @jdx)
Changed
-
New findings on existing workflows. The new
defaultandcorrectnessrules above, the newtemplate-injectioncontexts and the widercache-poisoningcoverage can report on workflows that passed on 2.1.0.actions-lockreports only in repositories with an Actions lock file. Popular actions pinned to a full version, or to a commit with a version comment, now get their inputs checked. (#174, @jdx) -
Stricter syntax checks. These are now reported because GitHub rejects them: a
scheduleentry withoutcron,merge_grouptypes other thanchecks_requested,$/path@ref,$/paths that leave the repository or don't exist,timeout-minutesin composite steps, unsupported contexts in action input defaults, and expressions inuses:. (#186, @jdx) -
Runner label retirements:
unknown-runner-labelwill start reportingmacos-14on 2026-11-02 andubuntu-22.04on 2027-04-17. Each finding names the retirement date and replacement labels. Before those dates, only the pedanticdeprecated-runner-labelwarns. (#182, @jdx) -
Fewer default findings:
dangerous-triggersreports a workflow that has no write scopes and reads no secrets only withpedantic. (#189)cache-poisoningreportsdocker/setup-buildx-actiononly whenversionis set.pedanticrestores the old behavior. (#160)- Hosted-runner provider labels are no longer unknown. (#199)
All by @jdx.
-
A broken
extendsis now an error. In a discovered config file, anextendsentry that points to a missing file exits with status 3. Before, the whole config was silently skipped. (#285, @jdx) -
Ignore comments inside scripts no longer apply. An own-line
# jactionlint ignore=inside arun: |block or another multi-line string has no effect. Only real YAML comments are read. (#284, @jdx) -
Go API:
NewLinter(out, nil)no longer panics.Error.PrettyPrintprints the rule ID instead of the kind.Error()is unchanged.ExprSemanticsCheckerrejects every context unlessSetContextAvailabilityis called.
Fixed
- False positives:
- Missing findings:
--changed-sincecounts findings that several files share once, keeps..foopaths inside the repository, and refuses--export-facts. (#279)download-jactionlint.bash: a missing release now prints a clear error instead of a gzip error. The script acceptsamd64(FreeBSD), and its help text is corrected. (#282)--helpand messages:--helpand several online and--migrate-ignoresmessages now match the documented behavior. (#287)
All by @jdx.
Performance
- Peak memory is lower on very large repositories because fewer workflows are checked at the same time. (#158)
- Each run script is analyzed once, the callers of an action are parsed once, and parsed expressions are shared between rules. (#270, #165)
- The
--onlinechecks read the advisories of all actions with one request. (#268)
All by @jdx.
Full Changelog: v2.1.0...v2.2.0
💚 Sponsor jactionlint
jactionlint is a fork of rhysd/actionlint, maintained by @jdx, an open source developer at entire.io, the title sponsor of his open source work.
If jactionlint has a place in your development workflow, please consider becoming an individual or company sponsor. Your support funds ongoing development and helps keep jactionlint fast, free, and independent.